package zack.project.domain.strategy.service.armory;

import zack.project.domain.strategy.adapter.repository.IStrategyRepository;
import zack.project.domain.strategy.model.entity.StrategyAwardEntity;
import zack.project.domain.strategy.model.entity.StrategyEntity;
import zack.project.domain.strategy.model.entity.StrategyRuleEntity;
import zack.project.types.common.Constants;
import zack.project.types.enums.ResponseCode;
import zack.project.types.exception.AppException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.math.BigDecimal;
import java.security.SecureRandom;
import java.util.*;

/**抽奖策略装配和库存调度实现类
 * @author A1793
 */
@Service
@Slf4j
public class StrategyArmoryDispatch implements IStrategyArmory, IStrategyDispatch {

    @Resource
    private IStrategyRepository repository;

    private final SecureRandom random = new SecureRandom();

    /**
     * 根据活动id来装配抽奖策略,strategyId和activityId是一一对应的
     * @param activityId
     */
    @Override
    public void assembleLotteryStrategyByActivityId(Long activityId) {
        Long strategyId =  repository.queryStrategyIdByActivityId(activityId);
        assembleLotteryStrategy(strategyId);
    }

    /**
     * 根据策略id去查询strategy_award奖品列表，
     * 先进行无权重概率表的装配（装配表的时候也要缓存一份rate_range，防止分布式下无法获取随机数的范围），之后再装配带权重的概率表
     * 先根据策略id去查询{strategy}获取rule_models，看这个策略下是否配置了rule_weight，如果配置了，则去strategy_rule表下，
     * 查找该策略id的权重配置（获取rule_value），之后将rule_value封装成一个Map<抽奖次数：奖品id1，奖品id2···，奖品id列表>>，
     * 将对应的key下的奖品列表和rateRange存到缓存中
     * 后面责任链进行权重节点过滤的时候，
     * 会根据策略id去查询strategy_rule表中该策略的权重配置(rule_value)并封装成Map<抽奖次数，抽奖次数：奖品id1，奖品id2···>，
     * 之后他找到自己小于当前抽奖次数的最大抽奖次数(strategy_rule中rule_value中的配置)，根据这个次数获取访问redis权重概率表的key
     * @param strategyId
     * @return
     */
    @Override
    public boolean assembleLotteryStrategy(Long strategyId) {
        //预热抽奖奖品配置
        List<StrategyAwardEntity> strategyAwardEntityList = repository.queryStrategyAwardList(strategyId);

        //向缓存中写入奖品的库存
        for(StrategyAwardEntity strategyAwardEntity : strategyAwardEntityList) {
            Integer awardId = strategyAwardEntity.getAwardId();
            //奖品库存总量
            Integer awardCount = strategyAwardEntity.getAwardCount();
            //在redis中设置 <cacheKey("strategy_award_count_key_#{strategyId}_#{awardId}),awardCount>
            //当决策树节点RuleStockLogicTreeNode在库存扣减以及设置库存余量锁时,会使用到此处预装配的库存总量
            cacheStrategyAwardCount(strategyId,awardId,awardCount);
        }


        //预热strategyId对应的抽奖概率配置(无权重的概率表)
        assembleLotteryStrategy(String.valueOf(strategyId), strategyAwardEntityList);

        //todo 概率表中可能会因为同一个权重下(抽奖次数)奖品的中奖概率之和不为1,导致概率表出现(i,null)的entry,会返回空的awardId


        //查询strategy总表来看是否配置了rule_weight
        StrategyEntity strategyEntity = repository.queryStrategyEntityByStrategyId(strategyId);
        String ruleWeight = strategyEntity.getRuleWeight();
        //如果为空则说明没有配置rule_weight
        if(null == ruleWeight){return true;}
        //在数据库表{strategy_rule}中查询strategyRuleEntity
        StrategyRuleEntity  strategyRuleEntity = repository.queryStrategyRule(strategyId,ruleWeight);
        if (null == strategyRuleEntity) {
            throw new AppException(ResponseCode.STRATEGY_RULE_WEIGHT_IS_NULL.getCode(), ResponseCode.STRATEGY_RULE_WEIGHT_IS_NULL.getInfo());
        }
        //key(抽奖次数:奖品id1,奖品id2···),value(奖品id1,奖品id2··)
        Map<String, List<Integer>> ruleWeightValueMap = strategyRuleEntity.getRuleWeightValues();
        Set<String> keys = ruleWeightValueMap.keySet();
        //为每个权重组设置权重概率表
        for (String key : keys) {
            List<Integer> ruleWeightValues = ruleWeightValueMap.get(key);
            ArrayList<StrategyAwardEntity> strategyAwardEntityList1 =
                    new ArrayList<>(strategyAwardEntityList);
            //在奖品列表中删除权重规定的奖品id中没有出现奖品
            strategyAwardEntityList1.removeIf(entity ->
                    !ruleWeightValues.contains(entity.getAwardId()));
            //再缓存一份权重抽奖概率配置和权重总坑位数,cacheKey("big_market_strategy_rate_table_key#{strategyId}_#{key}"
            assembleLotteryStrategy(String.valueOf(strategyId).concat(Constants.UNDERLINE).concat(key),
                    strategyAwardEntityList1);
        }
        return true;

    }

    private void assembleLotteryStrategy(String key, List<StrategyAwardEntity> strategyAwardEntityList) {

        //获取这些奖品里的rate最小值
        BigDecimal minAwardRate = strategyAwardEntityList.stream()
                .map(StrategyAwardEntity::getAwardRate)
                .min(BigDecimal::compareTo)
                .orElse(BigDecimal.ZERO);

        //计算一共有多少个坑位,比如当total为1且min值为0.01时,此时1%0.01=100,
        // 则有一百个坑位。向这些坑位填充awardid,这些坑位中的某个id数量可以表示这类id的概率
        BigDecimal rateRange = BigDecimal.valueOf(convert(minAwardRate.doubleValue()));
        //策略奖品概率查找表「这里指需要在list集合中,存放上对应的奖品占位即可,占位越多等于概率越高」
        List<Integer> strategyAwardSearchRateTables = new ArrayList<>(rateRange.intValue());

        //遍历strategyAwardEntityList,
        // 通过把其中的某个元素的rate属性值与raterange相乘可以获得要往strategyAwardSearchRateTables
        for (StrategyAwardEntity strategyAwardEntity : strategyAwardEntityList) {
            BigDecimal awardRate = strategyAwardEntity.getAwardRate();
            Integer awardId = strategyAwardEntity.getAwardId();
            //概率是0.01就放一次,0.99就放99次该奖品id(这个取决于最小概率)
            for (int i = 0; i < rateRange.multiply(awardRate).intValue(); i++) {
                strategyAwardSearchRateTables.add(awardId);
            }
        }

        //打乱概率查找表
        Collections.shuffle(strategyAwardSearchRateTables);

        //把list封装成map传给redis
        Map<Integer, Integer> shuffleStrategyAwardSearchRateTable = new LinkedHashMap<>();
        for (int i = 0; i < strategyAwardSearchRateTables.size(); i++) {
            shuffleStrategyAwardSearchRateTable.put(i, strategyAwardSearchRateTables.get(i));
        }
        //向缓存存入总坑位数和抽奖概率配置
        repository.storeStrategyAwardSearchRateTable(key,
                shuffleStrategyAwardSearchRateTable.size(),
                shuffleStrategyAwardSearchRateTable);
    }

    /**
     * 举例说明:
     * 当奖品列表中的最小概率是0.001时,max是1,此时是1:1000,此方法就是要扩大这个max一直到min为1,
     * 此时的max就是总的奖品id坑位,最小概率的奖品id只有1个,剩余的999个坑位由其余奖品id填充,仍是1:1000
     *
     * @param min
     * @return
     */
    private double convert(double min) {
        double current = min;
        double max = 1;
        while (current < 1) {
            current = current * 10;
            max = max * 10;
        }
        return max;
    }

    /**
     * 在缓存中设置奖品库存初始值
     * @param strategyId
     * @param awardId
     * @param awardCount
     */
    private void cacheStrategyAwardCount(Long strategyId, Integer awardId, Integer awardCount) {

        String cacheKey = Constants.RedisKey.STRATEGY_AWARD_COUNT_KEY + strategyId +
                Constants.UNDERLINE +
                awardId;

        repository.cacheStrategyAwardCount(cacheKey,awardCount);

    }


    /**
     * 根据策略id进行随机抽奖
     * @param strategyId
     * @return
     */
    @Override
    public Integer getRandomAwardId(Long strategyId) {
        // 分布式部署下,不一定为当前应用做的策略装配。也就是值不一定会保存到本应用,而是分布式应用,所以需要从 Redis 中获取。
        int rateRange = repository.getRateRange(strategyId);
        // 通过生成的随机值,获取概率值奖品查找表的结果
        return repository.getStrategyAwardAssemble(String.valueOf(strategyId), random.nextInt(rateRange));
    }

    /**
     * 带权重的随机抽奖需要找权重概率表
     * @param strategyId
     * @param ruleWeightValue
     * @return
     */
    @Override
    public Integer getRandomAwardId(Long strategyId,String ruleWeightValue) {
        String key = String.valueOf(strategyId).concat(Constants.UNDERLINE).concat(ruleWeightValue);

        return  getRandomAwardId(key);
    }

    @Override
    public Integer getRandomAwardId(String key) {
        //获取总坑位数
        int rateRange = repository.getRateRange(key);
        return repository.getStrategyAwardAssemble(String.valueOf(key), random.nextInt(rateRange));
    }


    @Override
    public boolean subtractionAwardStock(Long strategyId, Integer awardId,Date endDateTime) {
        String cacheKey = Constants.RedisKey.STRATEGY_AWARD_COUNT_KEY + strategyId + Constants.UNDERLINE + awardId;
        return  repository.subtractionAwardStock(cacheKey,endDateTime);
    }
}
